至此,我们介绍了linux系统中常用命令的使用方法,简述了bash程序的使用方法和工作流程。在使用bash编写脚本程序时,熟练掌握这些工具的用法,往往能够达到事半功倍的效果。
本文将通过讲述一些实例,试着探讨bash脚本编程的技巧。需要说明的是,这里的技巧是多角度寻求解决方案的思路,是建立在对各种命令和bash编程技法深刻理解的基础之上的。
1、笔试题
先来看某公司的两个笔试题:
1、写脚本实现,可以用shell、perl等。在目录/tmp下找到100个以abc开头的文件,然后把这些文件的第一行保存到文件new中。
分析:寻找名字符合某个模式的文件可以用find
,但find
不能控制寻找到的文件数量,也许可以用for
循环控制一下,查看文件的第一行有许多方法,可以用head
、sed
等。
根据以上思路写出脚本:
#!/bin/bash
for name in `find /tmp -type f ! -empty -name 'abc*'`
do
head -1 $name >> new && ((i++))
[[ $i -eq 100 ]] && break
done
脚本中每次成功写入文件new
中一行内容就令变量i
自增,当i增长到100时,立即结束循环。
另一种方案:
#!/bin/bash
sed 100q <(head -qn1 $(find /tmp -type f -name 'abc*')) >new
此方案只有一条命令,也不难理解:$(find ...)
部分获得文件名列表,<(head ...)
部分获取每个文件的第一行(<(...)
的用法请看这里),最后sed 100q ... >new
取前100行写入文件new。
2、写脚本实现,可以用shell、perl等。把文件b中有的,但是文件a中没有的所有行,保存为文件c,并统计c的行数。
问题没什么可分析的,直接的解决方案:
#!/bin/bash
cat b|while read line
do
if ! grep -xq $line a;then
echo $line >>c
fi
done
wc -l c
脚本通过循环读取文件b中的每一行,判断该行,如果该行不属于文件a,则输出该行内容到文件c中,循环结束后用wc
统计文件c的行数。
另一种方案:
#!/bin/bash
grep -vxf a b|tee c|wc -l
此方案利用grep
的-f
选项将文件a中的每行最为匹配模式匹配文件b的内容,-v
表示不匹配,然后通过管道交给命令tee
写入文件c中,然后在通过管道将标准输出交给wc
命令统计行数。
2、清空日志
在使用linux服务器的过程当中,随着服务的长时间运行,有时会有删除服务日志的需求。由于日志文件正在被该服务所使用,并不能直接进行删除(准确说是:即使直接删除了,空间也没有得到释放,需要将服务重启),比较好的做法是利用重定向清空该文件(如:>some.log
),既释放了空间,也不用重启服务。
但当需要清空的文件较多时,手动一个一个清空文件也有许多不方便,不如将需求写成脚本。
方案1:
#!/bin/bash
for log in `find /logs -name 'access_*.log'`
do
>$i
done
脚本很容易理解,也可以用命令find /logs -name 'access_*.log' -exec cp /dev/null {} \;
。
但仍是一个文件执行一次,能不能一次性执行完呢?
方案2:
#!/bin/bash
find /logs -name 'access_*.log'|xargs tee
此方案巧妙的利用了命令xargs
和tee
将find找到的文件一次性清空。
3、分发
假设要对一个较大文件分别给不同的程序处理,并收集处理结果。
通常的处理的办法可能是串行的处理该文件,但如果各个程序需要较长的处理时间,串行处理将不能有效的利用机器的性能,如果不同的处理程序在后台并发运行,类似这样:cat file|command1 &
,cat file|command2 &
,cat file|command3 &
...
这样处理能充分发挥服务器性能,但它的一个问题是,如果文件较大,对内存的消耗也会很大。
一种解决方案是:
#!/bin/bash
cat file|tee >(command1) >(command2) ... > >(commandN)|cat
此种方案的一个问题是,多个处理结果是随机的,如果需要处理结果是有序的(比如按命令的顺序输出),则不能满足需求。
另外,tee
命令分发的速率是恒定的,所以只能按处理命令中最慢的速率分发,它们的输出将争用同一个管道,一定条件下,有可能造成死锁。
另一种解决方案:
#!/bin/bash
#定义处理函数
f() {
mkfifo p{i,o}{1,2,3}
command1 < pi1 > po1 &
command2 < pi2 > po2 &
command3 < pi3 > po3 &
tee pi{1,2} > pi3 &
cat po{1,2,3}
rm -f p{i,o}{1,2,3}
}
cat file | f
此方案利用命名管道(见这里)处理分发及汇总各命令的输出,然后通过cat依次读取处理后的结果。
4、并发
我们在描述重定向与管道的文章中讲述过一种并发方式,下面介绍另一种。
我们说过,命令替换
的问题是命令的立即执行然后等待结果,此时shell无法传入输入。bash使用一个称为进程替换
的功能来弥补这些不足,进程替换
实际上是命令替换
和管道
的组合,和命令替换
类似,bash运行一个命令,但令其运行于后台而不再等待其完成。关键在于Bash为这条命令打开了一个用于读和写的管道,并且绑定到一个文件名,最后展开为结果。
利用进程替换
的这一特性,可以想到另外一种并发的方式:
#!/bin/bash
#处理函数,假设该函数的处理结果有且只有一个值
sth_todo() {
#需要对第一个参数处理的命令
some_command $1
}
#文件数组,也可以是其他待处理数据
file_list=('<(sth_todo '{1..10}.log')') #展开形如:<(sth_todo 1.log) <(sth_todo 2.log) <(sth_todo 3.log) ...
#收集结果并赋值给数组
read -a result <<<$(eval cat "${file_list[@]}")
#输出
echo "${result[@]}"
脚本中需要注意的地方在于数组的赋值和eval
的运用(见这里)。
当然处理函数并不一定只有唯一结果,如有其他结果,只需将收集结果部分做相应更改即可。
5、数组交、并、差集
假定有需要取两个数组的交集(或并集、差集),简单的做法无非是两个循环对比两个数组中的每个值,取得相同的部分:
#!/bin/bash
list_1=(...)
list_2=(...)
for i in ${list_1[@]}
do
for j in ${list_2[@]}
do
[[ "$i" == "$j" ]] && echo $i
done
done
再看另一种方案:
list_1=(...)
list_2=(...)
#重置IFS值
IFS=$'\n'
#交集
grep -xf <(echo "${list_1[*]}") <<<"${list_2[*]}"
#并集
sort -u <<EOF
${ip[*]}
${ip_[*]}
EOF
#差集之一
grep -vxf <(echo "${list_1[*]}") <<<"${list_2[*]}"
#还原IFS
IFS=$' \t\n'
bash的一些特性和常用命令结合使用,使原本需要许多循环代码解决的问题变得“轻而易举”。但本例中,需要重点理解的是:IFS
在数组扩展中的特性,命令grep
和sort
的运用,以及进程替换
的使用。
6、大量数据处理
假如需要对大量小文件进行简单的文本替换,而文件量已达到不可一次性处理的程度(比如几百万个)。
此时如果采用一般的处理办法,例如
find . -name '*.html' -exec sed -i 's/xxxx/oooo/g' {} \;
或类似的命令,显然,这样一个文件接着一个文件串行处理将花费巨大的时间成本。
对于此类问题,需要在服务器性能和时间成本上做取舍,先给出处理方案:
#!/bin/bash
#取得待处理文件数组
A=($(find . -name '*.html'))
#每10000个文件后台循环处理
for((i=0;i<${#A[@]};i+=10000))
do
sed -i 's@xxxx@oooo@g' ${A[@]:$i:10000} &
done
#等待所有进程退出
wait
此方案假定服务器资源可以随意使用,只为达到时间效率最大化。如果需要控制服务器资源消耗(主要是IO性能),可以结合这一篇,控制并发的进程数量。
关于bash的文章,至此就告一段落了。
我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/dev...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。